package plug;

import static main.java.commons.Utils.filter;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;

import plug.creatures.IsPlugeablePredicat;

/*
 * Non Generic Class Loader (PluginLoader<P> would be a pain, see why?)
 */
public class PluginLoader {

	/**
	 * current classloader object for plugins
	 */
	private static ClassLoader loader= ClassLoader.getSystemClassLoader();

	/**
	 * directory where plugins are loaded from
	 */
	private String pluginDirectory;

	/**
	 * plugin cache (loaded plugin)
	 */
	private Map<String, Class<IPlugin>> loadedPluginClasses = new HashMap<String, Class<IPlugin>>();

	/**
	 * logger facilities to trace plugin loading...
	 */
	private static Logger logger = Logger.getLogger("plug.PluginLoader");

	/**
	 * type of the plugins (subtype of IPlugin), use to filter out plugins
	 */
	protected Class<? extends IPlugin> ptype;

	/**
	 * create a plugin loader, loading from directory
	 * 
	 * @param directory
	 *            loading location (out of classpath for reloading)
	 */
	public PluginLoader(String directory, Class<? extends IPlugin> type)
			throws MalformedURLException {
		File repository = new File(System.getProperty("user.dir"));
		repository = repository.getParentFile();
		directory = repository.getAbsolutePath() + "/" + directory;
		File dirFile = new File(directory);
		if (dirFile == null || !dirFile.isDirectory()) {
			String errmsg = directory + " is not a directory";
			logger.warning(errmsg);
			throw new IllegalArgumentException(errmsg);
		}
		// add an ending "/" if needed (classloader seems to need this)
		if (!directory.endsWith("/")) {
			directory += "/";
		}
		this.pluginDirectory = directory;
		ptype = type;
		
	}

	/**
	 * load all plugins of type ptype (does not change any plugin already
	 * loaded)
	 */
	public void loadPlugins() {
		// Indirection to the "directory" version, could be extended with Jar
		// support
		loadFromDirectory();
	}

	/**
	 * Reload all plugins of type P (erasing all previously loaded plugins)
	 */
	public void reloadPlugins() {
		erasePluginClasses();
		loadPlugins();
	}


	/**
	 * @return a new list of the currently loaded plugin classes
	 */
	public List<Class<IPlugin>> getPluginClasses() {
		return new ArrayList<Class<IPlugin>>(loadedPluginClasses.values());
	}

	/**
	 * delete all plugin classes from the cache
	 */
	private void erasePluginClasses() {
		loadedPluginClasses.clear();
	}

	/**
	 * Load plugins from the plugin directory
	 */
	private void loadFromDirectory() {
		logger.info("=+=+=+=+=+ Entry in loadFromDirectory=+=+=+=+");
		loadFromSubdirectory(new File(pluginDirectory), pluginDirectory);
		logger.info("=+=+=+=+=+ Exit from loadFromDirectory=+=+=+=+");
	}

	/**
	 * Load plugins in a sub-dir from the baseName (not in a jar)
	 * 
	 * @param baseName
	 *            name of the base dir (to determine package naming)
	 * @param dir
	 *            sub-directory, package naming of the plugin should correspond
	 *            to the relative position in the sub-dir
	 */
	@SuppressWarnings("rawtypes")
	private void loadFromSubdirectory(File dir, String baseName) {
		logger.info("Loading in subdir " + dir + " with basename " + baseName);
		File[] files = dir.listFiles();
		for (File file : filter(Arrays.asList(files),
				IsPlugeablePredicat.getInstance())) {
			String pathToJar = file.getAbsolutePath();
			JarFile jarFile;
			try {
				jarFile = new JarFile(pathToJar);

				Enumeration e = jarFile.entries();

				PluginLoader.addURLToSystemClassLoader(new URL("jar:file:" + pathToJar + "!/"));

				while (e.hasMoreElements()) {
					JarEntry je = (JarEntry) e.nextElement();
					if (je.isDirectory() || !je.getName().endsWith(".class")) {
						continue;
					}
					// -6 because of .class
					String className = je.getName().substring(0,
							je.getName().length() - 6);
					className = className.replace('/', '.');
					
					Class<IPlugin> plugin =loadOnePluginClass(className);
					if (plugin != null) {
						  boolean notLoaded = (loadedPluginClasses.get(plugin
						  .getName()) == null);
						  if (notLoaded) {
						  logger.info("Class " + className
						  + " is a new plugin!");
						  loadedPluginClasses.put(plugin.getName(), plugin);
						  } else {
						  logger.info("Class " + className
						  + " is already loaded, IGNORING!");
						  }
					}
				}

			} catch (IOException e1) {
				logger.warning("Class " + pathToJar +"is not a correct JAR file");
				e1.printStackTrace();
			}
		}
	}




	/**
	 * Load one class (of type ptype) from className
	 * 
	 * @param className
	 *            name of the plugin class
	 * @return plugin Class, or null if any problem
	 */
	@SuppressWarnings("unchecked")
	private Class<IPlugin> loadOnePluginClass(String className) {
		try {
			logger.info("Request for loading class " + className + " by "
					+ this);
			Class<?> loadedClass = loader.loadClass(className);
			// check that the class is of the right type
			if (ptype.isAssignableFrom(loadedClass)&& !loadedClass.isInterface() && !Modifier.isAbstract( loadedClass.getModifiers() ))  {
				return (Class<IPlugin>) loadedClass;
			} else {
				logger.warning("Class " + className
						+ " is not from the expected type" + ptype.getName());
			}
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			logger.warning("Plugin " + className + " not found");
		} catch (NoClassDefFoundError e) {
			e.printStackTrace();
			logger.warning("Class " + className + " not defined");
		}
		return null;
	}

	public Class<? extends IPlugin> getPtype() {
		return ptype;
	}

	public String getPluginDirectory() {
		return pluginDirectory;
	}
	
	private static  void addURLToSystemClassLoader(URL u) throws IOException { 
		URLClassLoader systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader(); 
		Class<URLClassLoader> classLoaderClass = URLClassLoader.class; 
	 
		try { 
			Method method = classLoaderClass.getDeclaredMethod("addURL", new Class[]{URL.class}); 
			method.setAccessible(true); 
			method.invoke(systemClassLoader, new Object[]{u}); 
		} catch (Throwable t) { 
			t.printStackTrace(); 
			throw new IOException("Impossible d'ajouter l'URL au ClassLoader. "); 
		} 
	}

}
